Conversation
|
这个争取明天看完哈,看了一下最终结果好像跟我提供那分支差不多。 问一下学员想法,觉得提供一个参考分支是不是有必要呢? |
EthanLin-TWer
left a comment
There was a problem hiding this comment.
为了达到原子提交的效果,这个提交最好和下个修改测试的提交合并一起提交。这样回退/回滚的时候测试仍然能在提交间保持运行通过。
前面做小调整修修的lint的节奏很好。
|
|
||
| public class Game { | ||
| ArrayList<String> players = new ArrayList<>(); | ||
| ArrayList<Player> tempPlayers = new ArrayList<>(); |
| public class Player { | ||
| private String name; | ||
| private int place = 0; | ||
| public int place = 0; |
There was a problem hiding this comment.
嗯,字段搬移过来的时候先设置为public是简化“替换使用点(getter)”的常见办法,可以使替换getter的时候步子变得更小。
|
|
||
| private void gainGoldCoin() { | ||
| goldCoins[currentPlayer]++; | ||
| tempPlayers.get(currentPlayer).goldCoin++; |
There was a problem hiding this comment.
不知道这个地方做的时候会不会感觉步子大?
一般套路是在setter的地方同样“旧的不变,新的创建”一下,再把getter替换到新的字段上,最后删除老的setter:
goldCoins[currentPlayer]++;
tempPlayers.get(currentPlayer).goldCoin++; // 先同时set到新旧两个地方然后再做下面getter部分的替换,最后删除上面第一行的旧setter。
| public class Game { | ||
| ArrayList<String> players = new ArrayList<>(); | ||
| ArrayList<Player> tempPlayers = new ArrayList<>(); | ||
| int[] goldCoins = new int[6]; |
There was a problem hiding this comment.
还没看后面的提交,不过这个地方完成替换以后一般习惯是先用getGoldCoin()把public字段先封装起来。这个提交 12a280a 也是一样的。
| public void gainGoldCoin() { | ||
| goldCoin++; | ||
| System.out.println(name + " now has " + goldCoin + " Gold Coins."); | ||
| } |
There was a problem hiding this comment.
这个地方可不必手搬,见下个commit comment。
| + tempPlayers.get(currentPlayer).goldCoin | ||
| + " Gold Coins."); | ||
| } | ||
|
|
There was a problem hiding this comment.
这三个提交做的事是把gainGoldCoin()的行为搬移到Player上,在搬移上有自动化的手法/快捷键可以提高效率,也是一种常见的处理思路:
- 先利用“提炼变量”(《重构2》6.3节, cmd + option + v/ctrl + alt + v)提炼出
currentPlayer来:
private void gainGoldCoin() {
final Player player = tempPlayers.get(currentPlayer);
player.goldCoin++;
System.out.println(getCurrentPlayer()
+ " now has "
+ player.goldCoin
+ " Gold Coins.");
}- 将剩余部分的方法体利用“提炼方法”(《重构2》6.1节,cmd + option + m/ctrl + alt + m)提炼出去:
private void gainGoldCoin() {
final Player player = tempPlayers.get(currentPlayer);
gainGoldCoin(player);
}
private void gainGoldCoin(Player player) {
player.goldCoin++;
System.out.println(getCurrentPlayer()
+ " now has "
+ player.goldCoin
+ " Gold Coins.");
}- 对
getCurrentPlayer()方法应用“内联函数”(《重构2》6.2节,cmd + option + n/control + alt + n),因为getCurrentPlayer()对Player为private不可见:
private void gainGoldCoin(Player player) {
player.goldCoin++;
System.out.println(player.getName()
+ " now has "
+ player.goldCoin
+ " Gold Coins.");
}- 至此
gainGoldCoin()方法中的数据都来源于Player,对该方法应用“搬移函数”(《重构2》8.1节,F6),将方法搬移到Player类上:
private void gainGoldCoin() {
final Player player = tempPlayers.get(currentPlayer);
player.gainGoldCoin();
}- 完成搬移,对方法做些后处理:先inline
player,再inline整个方法:
private void gainGoldCoin() {
tempPlayers.get(currentPlayer).gainGoldCoin();
} public boolean wasCorrectlyAnswered() {
if (tempPlayers.get(currentPlayer).isInPenaltyBox) {
if (isGettingOutOfPenaltyBox) {
...
nextPlayer();
tempPlayers.get(currentPlayer).gainGoldCoin();
return didPlayerWin();
} else { ... }
} else { ... }
}这里有两个点:
- “先提炼,后内联”:提炼出可供自动化搬移的结构,搬移后再内联回来。同样也是对过程的设计
- 过程需要处理下不在目标函数作用域中的变量/函数,在上面就是
getCurrentPlayer()
手法虽然看起来很长,但其实练熟了速度更快,还不容易出错。
|
|
||
| public boolean isWin() { | ||
| return goldCoin >= 6; | ||
| } |
There was a problem hiding this comment.
isWin()可跟上述手法同理搬运。然后搞到这就发现两次为了搬运做了提炼getCurrentPlayer()方法的操作,不如重构完就提炼出这个方法。我做完这个提炼后发现一共有10处地方在用getCurrentPlayer(),也能一定程度为后面“搬移哪些字段”做提示。
There was a problem hiding this comment.
后面提交还有些也同理,都可以用这个方法进行搬运,如penaltyBox几个处理方法那块。
| System.out.println("The category is " + currentCategory.getValue()); | ||
| System.out.println(questionMap.get(currentCategory).removeFirst()); | ||
| } | ||
|
|
There was a problem hiding this comment.
这里相当于直接抄了份新方法,改完以后直接把调用点切换过去,而没有在新旧方法中间建立桥梁。那如果这次调用点切换完测试挂了,你能怎么办?回滚copy的所有新东西?调试?因没有搭建桥梁故,所以这种手法不够小步。
我可能会这么做:
- 先对原来的
currentCategory()应用提炼函数(6.1, cmd + option + m/control + alt + m):
private String currentCategory() {
return temp_currentCategory();
}
private String temp_currentCategory() {
if (getCurrentPlace() == 0) return "Pop";
if (getCurrentPlace() == 4) return "Pop";
if (getCurrentPlace() == 8) return "Pop";
if (getCurrentPlace() == 1) return "Science";
if (getCurrentPlace() == 5) return "Science";
if (getCurrentPlace() == 9) return "Science";
if (getCurrentPlace() == 2) return "Sports";
if (getCurrentPlace() == 6) return "Sports";
if (getCurrentPlace() == 10) return "Sports";
return "Rock";
}- 修改
temp_currentCategory()的返回值类型为Category,在旧方法中间将其转换为String并保持功能不变:
private String currentCategory() {
return temp_currentCategory().getValue();
}
private Category temp_currentCategory() {
if (getCurrentPlace() == 0) return Category.POP;
if (getCurrentPlace() == 4) return Category.POP;
if (getCurrentPlace() == 8) return Category.POP;
if (getCurrentPlace() == 1) return Category.SCIENCE;
if (getCurrentPlace() == 5) return Category.SCIENCE;
if (getCurrentPlace() == 9) return Category.SCIENCE;
if (getCurrentPlace() == 2) return Category.SPORTS;
if (getCurrentPlace() == 6) return Category.SPORTS;
if (getCurrentPlace() == 10) return Category.SPORTS;
return Category.ROCK;
}- 对调用点也先做“旧的不变,新的创建”的操作:
private void askQuestion() {
String currentCategory = currentCategory();
+ Category temp = Category.valueOf(currentCategory.toUpperCase());
System.out.println("The category is " + currentCategory);
...
}- 先替换一个调用点试试水:
private void askQuestion() {
String currentCategory = currentCategory();
Category temp = Category.valueOf(currentCategory.toUpperCase());
- System.out.println("The category is " + currentCategory);
+ System.out.println("The category is " + temp.getValue());
...
}- 4测试通过的话再替换其他引用点,不通过的话出错范围只有1行,回滚之找找原因:
private void askQuestion() {
- String currentCategory = currentCategory();
Category temp = Category.valueOf(currentCategory.toUpperCase());
System.out.println("The category is " + temp.getValue());
if (temp == Category.POP)
System.out.println(questionMap.get(Category.POP).removeFirst());
if (temp == Category.SCIENCE)
System.out.println(questionMap.get(Category.SCIENCE).removeFirst());
if (temp == Category.SPORTS)
System.out.println(questionMap.get(Category.SPORTS).removeFirst());
if (temp == Category.ROCK)
System.out.println(questionMap.get(Category.ROCK).removeFirst());
}- 清理,先内联
String currentCategory变量,最后删除无用的currentCategory()并将temp_currentCategory()重命名回来:
private void askQuestion() {
Category temp = temp_currentCategory();
System.out.println("The category is " + temp.getValue());
if (temp == Category.POP)
System.out.println(questionMap.get(Category.POP).removeFirst());
if (temp == Category.SCIENCE)
System.out.println(questionMap.get(Category.SCIENCE).removeFirst());
if (temp == Category.SPORTS)
System.out.println(questionMap.get(Category.SPORTS).removeFirst());
if (temp == Category.ROCK)
System.out.println(questionMap.get(Category.ROCK).removeFirst());
}这里的重点是要在新旧函数中间搭建调用桥梁,这样后续才能每改一两行代码都跑测试验证。copy整份方法及调用点并直接替换的方式,在方法更长、更复杂、调用点更多的场景下就会步子很大,增加出错概率和排查范围。
| SPORTS("Sports"), | ||
| ROCK("Rock"), | ||
| BLUES("Blues"), | ||
| HISTORY("History"); |
| public Category getCurrentCategory(int place) { | ||
| int index = place % Category.values().length; | ||
| return Category.values()[index]; | ||
| } |
|
|
||
| private void askQuestion() { | ||
| Category currentCategory = currentCategory(); | ||
| Category currentCategory = Category.getCurrentCategory(getCurrentPlace()); |
There was a problem hiding this comment.
方法名是否可尝试简短些?例如:
Category currentCategory = Category.in(getCurrentPlace());| public int place = 0; | ||
| public int goldCoin = 0; | ||
| private int place = 0; | ||
| private int goldCoin = 0; |
|
Review done。 一些地方的手法还有可打磨的地方,不过总体的步子和手法应用还是很不错的。在执行“旧的不变,新的创建”时,需要更多依赖快捷键和IDE帮你做提炼并搭建好中间的调用桥梁,不需要手动去做创建,这样有利于进一步缩小步子,使重构更可控。 继续精益求精。很棒哟~ |
bug修复的时候有几步有点大。。